/**
 * This file is part of Sonedyan.
 * 
 * Sonedyan is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation;
 * either version 3 of the License, or (at your option) any
 * later version.
 *
 * Sonedyan is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public
 * License along with Octave; see the file COPYING.  If not
 * see <http://www.gnu.org/licenses/>.
 * 
 * Copyright (C) 2009-2012 Jimmy Dubuisson <jimmy.dubuisson@gmail.com>
 */

package org.unige.mpej.eckmann.sonedyan.fns.utils;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;

import org.apache.log4j.Logger;
import org.unige.mpej.eckmann.sonedyan.fns.bean.Actor;
import org.unige.mpej.eckmann.sonedyan.fns.bean.Link;
import org.unige.mpej.eckmann.sonedyan.fns.bean.Actor.Status;
import org.unige.mpej.eckmann.sonedyan.fns.graph.GraphMetrics;
import org.unige.mpej.eckmann.sonedyan.utils.GraphUtils;

import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.SparseMultigraph;

/**
 * set of util methods for managing produced data
 */
public class DataUtils
{
	private static Logger log = Logger.getLogger(org.unige.mpej.eckmann.sonedyan.fns.utils.DataUtils.class);
	
	// inner class for storing temporary data 
	private static class TimeStatus implements Comparable<TimeStatus>
	{
		private Status status;
		private Date startDate;
		private Date endDate;
		
		public Status getStatus()
		{
			return status;
		}

		public void setStatus(Status status)
		{
			this.status = status;
		}

		public Date getStartDate()
		{
			return startDate;
		}

		public void setStartDate(Date startDate)
		{
			this.startDate = startDate;
		}

		public Date getEndDate()
		{
			return endDate;
		}

		public void setEndDate(Date endDate)
		{
			this.endDate = endDate;
		}
		
		/**
		 * compareTo implementation
		 */
		public int compareTo(TimeStatus status)
		{
			if (this.startDate.getTime() > status.getStartDate().getTime())
			{
				return 1;
			}
			else if (this.startDate.getTime() < status.getStartDate().getTime())
			{
				return -1;
			}
			else
			{
				return 0;
			}
		}
		
	}
	
	/**
	 * get the set of actor ids having a specified status for a set of defined dates
	 */
	public static Hashtable<Date, Vector<String>> getActorStatusIdsInDateRange(Hashtable<String, TreeMap<Date, Vector<Status>>> map, Date startDate, Date endDate, Status status)
	{
		try
		{
			Hashtable<Date, Vector<String>> timeActorIds = new Hashtable<Date, Vector<String>>(); 
			Set<String> idSet = map.keySet();
			
			SimpleDateFormat formatter = new SimpleDateFormat(org.unige.mpej.eckmann.sonedyan.utils.DataUtils.REDUCED_DATE_FORMAT);
			
			Date date = startDate;
			
			Calendar cal = Calendar.getInstance(); 
			cal.setTime(date);
			
			log.debug("Getting actor ids for status " + status + " between " + formatter.format(startDate) + " and " + formatter.format(endDate));
			
			while (date.getTime() <= endDate.getTime())
			{
				for (String id : idSet)
				{
					TreeMap<Date, Vector<Status>> timeStatus = map.get(id);
					
					if (timeStatus != null)
					{
						Vector<Status> statusVector = timeStatus.get(date);
						
						if (statusVector != null && statusVector.contains(status))
						{
							Vector<String> actorIds = timeActorIds.get(date);
							
							if (actorIds == null){ actorIds = new Vector<String>(); }
							
							actorIds.add(id);
							timeActorIds.put(date, actorIds);
						}
					}
				}
				
				// add one month to the current date
				cal.add(Calendar.MONTH, 1);
				date = cal.getTime();
			} // while
			
			return timeActorIds;
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get actor time status map
	 * 
	 * NB: status SHOULD evolve in the following direction over time: PhD -> PostDoc -> Professor
	 */
	public static Hashtable<String, TreeMap<Date, Vector<Status>>> getActorTimeStatusMap(SparseMultigraph<Actor, Link> multiGraph)
	{
		try
		{
			Hashtable<String, Vector<TimeStatus>> actorTimeStatus = new Hashtable<String, Vector<TimeStatus>>();
			Hashtable<String, TreeMap<Date, Vector<Status>>> actorTimeStatusMap = new Hashtable<String, TreeMap<Date, Vector<Status>>>();
			
			ArrayList<Link> list = new ArrayList<Link>(multiGraph.getEdges());
			
			for (Link e : list)
			{
				// from actor
				String fromActorId = e.getFromActorId();
				Vector<TimeStatus> timeStatusVector = actorTimeStatus.get(fromActorId);
				
				if (timeStatusVector == null)
				{
					timeStatusVector = new Vector<TimeStatus>();
				}
				
				TimeStatus tStatus = new TimeStatus();
				
				tStatus.setStatus(e.getFromStatus());
				tStatus.setStartDate(e.getStartDate());
				tStatus.setEndDate(e.getEndDate());
				
				timeStatusVector.add(tStatus);
				actorTimeStatus.put(fromActorId, timeStatusVector);
				
				// to actor
				String toActorId = e.getToActorId();
				timeStatusVector = actorTimeStatus.get(toActorId);
				
				if (timeStatusVector == null)
				{
					timeStatusVector = new Vector<TimeStatus>();
				}
				
				tStatus = new TimeStatus();
				
				tStatus.setStatus(e.getToStatus());
				tStatus.setStartDate(e.getStartDate());
				tStatus.setEndDate(e.getEndDate());
				
				timeStatusVector.add(tStatus);
				actorTimeStatus.put(toActorId, timeStatusVector);
			} // for
			
			Set<String> keys = actorTimeStatus.keySet();
			
			// for all actors
			for (String actorId : keys)
			{
				// get the old status vector
				Vector<TimeStatus> oldStatusVector = actorTimeStatus.get(actorId);
				
				// log.debug("Computing status vector (size " + oldStatusVector.size() + ") of actor: " + actorId);
				
				// splitting PHD status
				Vector<TimeStatus> phdStatusVector = new Vector<TimeStatus>();
				
				for (TimeStatus status : oldStatusVector)
				{
					if (status.getStatus() == Status.PHD)
					{
						phdStatusVector.add(status);
					}
				}
				
				// splitting POSTDOC status
				Vector<TimeStatus> postdocStatusVector = new Vector<TimeStatus>();
				
				for (TimeStatus status : oldStatusVector)
				{
					if (status.getStatus() == Status.POSTDOC)
					{
						postdocStatusVector.add(status);
					}
				}
				
				// splitting PROFESSOR status
				Vector<TimeStatus> professorStatusVector = new Vector<TimeStatus>();
				
				for (TimeStatus status : oldStatusVector)
				{
					if (status.getStatus() == Status.PROFESSOR)
					{
						professorStatusVector.add(status);
					}
				}
				
				// log.debug("Actor PHD status size: " + phdStatusVector.size());
				// log.debug("Actor POSTDOC status size: " + postdocStatusVector.size());
				// log.debug("Actor PROFESSOR status size: " + professorStatusVector.size());
				
				TreeMap<Date, Vector<Status>> map = new TreeMap<Date, Vector<Status>>();
				
				// PHD status
				for (TimeStatus status : phdStatusVector)
				{
					Date date = status.getStartDate();
					
					Calendar cal = Calendar.getInstance(); 
					cal.setTime(date);
					
					while (date.getTime() <= status.getEndDate().getTime())
					{
						Vector<Status> phd = map.get(date);
						
						if (phd == null)
						{ 
							phd = new Vector<Status>(); 
							phd.add(Status.PHD);
						}
						else if (!phd.contains(Status.PHD))
						{
							phd.add(Status.PHD);
						}
						
						map.put(date, phd);
						
						// add one month to the current date
						cal.add(Calendar.MONTH, 1);
						date = cal.getTime();
					}
				}
				
				// POSTDOC status
				for (TimeStatus status : postdocStatusVector)
				{
					Date date = status.getStartDate();
					
					Calendar cal = Calendar.getInstance(); 
					cal.setTime(date);
					
					while (date.getTime() <= status.getEndDate().getTime())
					{
						Vector<Status> postdoc = map.get(date);
						
						if (postdoc == null)
						{ 
							postdoc = new Vector<Status>(); 
							postdoc.add(Status.POSTDOC);
						}
						else if (!postdoc.contains(Status.POSTDOC))
						{
							postdoc.add(Status.POSTDOC);
						}
						
						map.put(date, postdoc);
						
						// add one month to the current date
						cal.add(Calendar.MONTH, 1);
						date = cal.getTime();
					}
				}
				
				// PROFESSOR status
				for (TimeStatus status : professorStatusVector)
				{
					Date date = status.getStartDate();
					Calendar cal = Calendar.getInstance(); 
					cal.setTime(date);
					
					// log.debug("Getting professor status for actor " + actorId + " between " + formatter.format(status.getStartDate()) + " and " + formatter.format(status.getEndDate()));
					
					while (date.getTime() <= status.getEndDate().getTime())
					{
						Vector<Status> professor = map.get(date);
						
						if (professor == null)
						{ 
							professor = new Vector<Status>(); 
							professor.add(Status.PROFESSOR);
						}
						else if (!professor.contains(Status.PROFESSOR))
						{
							professor.add(Status.PROFESSOR);
						}
						
						map.put(date, professor);
						
						// add one month to the current date
						cal.add(Calendar.MONTH, 1);
						date = cal.getTime();
					}
				}
				
				// log.debug("Adding map of size " + map.size() + " to actor " + actorId);
				
				actorTimeStatusMap.put(actorId, map);
			} // for
			
			return actorTimeStatusMap;
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * export time series to arff format
	 */
	public static void exportStatusTimeSeries2Arff(String fileName, Hashtable<String, TreeMap<Date, Double>> timeSeries, SparseMultigraph<Actor, Link> multiGraph, long timeLag)
	{
		PrintWriter out = null;
	
		try
		{
			Set<String> ids = timeSeries.keySet();
			
			log.info("Exporting status time series (# vertices: '" + ids.size() + "') to Arff format...");
			
			out = new PrintWriter(new BufferedWriter(new FileWriter(fileName)));
			
			// see http://weka.wikispaces.com/ARFF+%28stable+version%29
			out.print("% FNS data subgraph node time series with status class\n\n");
			
			out.print("@RELATION fnsProjectParticipationSubGraphNodesTimeSeries\n\n");
			
			boolean writtenDateAttributes = false;
			Hashtable<Date, Graph<Actor, Link>> snapshotSubGraphs = new Hashtable<Date, Graph<Actor, Link>>();
			
			// store status history for nodes whose status changes over time
			Hashtable<String, HashMap<Date, Vector<Status>>> nodesStatusHistory = new Hashtable<String, HashMap<Date, Vector<Status>>>();
			
			for (String id : ids)
			{
				TreeMap<Date, Double> nodeTimeSeries = timeSeries.get(id);
				
				Set<Date> dates = nodeTimeSeries.keySet();
				
				if (!writtenDateAttributes)
				{
					int counter = 1;
					
					for (Date d : dates)
					{
						out.println("@ATTRIBUTE t" + counter + " real");
						counter++;
					}
					
					out.println("@ATTRIBUTE nodeStatus {PHD, POSTDOC, PROFESSOR}\n");
					out.println("@DATA");
					
					writtenDateAttributes = true;
				}
				
				boolean toBeIgnored = false;
				StringBuffer timeSeriesValues = new StringBuffer();
				Status fixedStatus = null;
				Graph<Actor, Link> snapshotGraph;
				Graph<Actor, Link> snapshotSubGraph;
				LinkedHashMap<Date, Vector<Status>> nodeStatusHistory = new LinkedHashMap<Date, Vector<Status>>();
				
				for (Date d : dates)
				{
					snapshotSubGraph = snapshotSubGraphs.get(d);
					
					// store the extracted snapshot sub graphs in memory to reduce future computation
					if (snapshotSubGraph == null)
					{
						snapshotGraph = GraphUtils.getMultiGraphSnapshot(multiGraph, d.getTime(), timeLag);
						snapshotSubGraph = GraphUtils.getSubGraph(snapshotGraph, ids);
						snapshotSubGraphs.put(d, snapshotSubGraph);
					}
					
					// get the node status in the extracted snapshot graph
					// NB: it might be impossible to get the node status for certain snapshots
					// NB: in such case, nodeStatus is null
					Vector<Status> nodeStatus = GraphMetrics.getNodeStatus(snapshotSubGraph, id);
					
					// store the node status
					nodeStatusHistory.put(d, nodeStatus);
					
					if (nodeStatus != null)
					{
						if (nodeStatus.size() == 1)
						{
							Status status = nodeStatus.get(0);
							
							if (fixedStatus == null)
							{
								fixedStatus = status;
							}
							// node has multiple status over the studied period
							else if (fixedStatus != status)
							{
								toBeIgnored = true;
								// log.info("Node '" + id + "' has multiple status over the studied period...");
							}
						}
						// node has multiple concurrent status
						else if (nodeStatus.size() > 1)
						{ 
							toBeIgnored = true; 
							// log.info("Node '" + id + "' has multiple concurrent status at date '" + d + "'...");
						}
					}
					
					timeSeriesValues.append(nodeTimeSeries.get(d) + ",");
				} // for all dates
				
				// write node time series
				if (!toBeIgnored)
				{
					out.println(timeSeriesValues.toString() + fixedStatus);
				}
				// save node status history
				else
				{
					nodesStatusHistory.put(id, nodeStatusHistory);
				}
			}
			
			log.debug("Export finished...");
			
			Set<String> changingIds = nodesStatusHistory.keySet();
			SimpleDateFormat formatter = new SimpleDateFormat(org.unige.mpej.eckmann.sonedyan.utils.DataUtils.DISPLAY_DATE_FORMAT);
			
			for (String id : changingIds)
			{
				HashMap<Date, Vector<Status>> nodeStatusHistory = nodesStatusHistory.get(id);
				
				Set<Date> dates = nodeStatusHistory.keySet();
				
				log.info("Displaying Node '" + id + "' status history...");
				
				Status oldStatus = null;
				StringBuffer history = new StringBuffer();
				
				for (Date d : dates)
				{
					Vector<Status> status = nodeStatusHistory.get(d);
					
					if (status != null)
					{
						if (status.size() == 1)
						{
							Status newStatus = status.get(0);
							
							// display status only if different than previous one
							if (oldStatus != newStatus)
							{
								oldStatus = newStatus;
								history.append(formatter.format(d) + ": " + status + "\n");
							}
						}
						else
						{
							history.append(formatter.format(d) + ": " + status + "\n");
						}
					}
				} // for
				
				log.info(history.toString());
			}
			
			out.close();
		}
		catch(Exception e)
		{
			log.error("Unable to export time series to arff format: " + e.getMessage());
		}
		finally
		{
			try
			{
				if (out != null)
				{
					out.close();
				}
			}
			catch(Exception e){}
		}
	}
	
	/**
	 * load the status time series and get the specified average status time series
	 */
	public static Vector<Double> getAvgStatusTimeSeries(String filePath, Status status)
	{
		BufferedReader br = null;
		Vector<Double> avgTimeSeries = new Vector<Double>();
		int counter = 0;
		
		try
		{
			br = new BufferedReader(new FileReader(filePath));
			
			String line;
			
			while ((line = br.readLine()) != null)
			{
				if (line.length() > 0 && !line.startsWith("%") && !line.startsWith("@"))
				{
					String[] columns = line.split(",");
					Vector<Double> timeSeries = new Vector<Double>();
					
					String statusString = columns[columns.length - 1];
					
					if (statusString.equals("PHD") && status == Status.PHD)
					{
						for (int i = 0; i < columns.length - 1; i++)
						{
							timeSeries.add(new Double(columns[i]));
							counter++;
						}
					}
					else if (statusString.equals("POSTDOC") && status == Status.POSTDOC)
					{
						for (int i = 0; i < columns.length - 1; i++)
						{
							timeSeries.add(new Double(columns[i]));
							counter++;
						}
					}
					else if (statusString.equals("PROFESSOR") && status == Status.PROFESSOR)
					{
						for (int i = 0; i < columns.length - 1; i++)
						{
							timeSeries.add(new Double(columns[i]));
							counter++;
						}
					}
					
					if (timeSeries.size() > 0)
					{
						if (avgTimeSeries.size() == 0)
						{
							avgTimeSeries = timeSeries;
						}
						else
						{
							for (int i = 0; i < avgTimeSeries.size(); i++)
							{
								Double value1 = avgTimeSeries.elementAt(i);
								Double value2 = timeSeries.elementAt(i);
								
								avgTimeSeries.setElementAt(value1 + value2, i);
							}
						}
					}
				}
			} // while
			
			for (int i = 0; i < avgTimeSeries.size(); i++)
			{
				Double value = avgTimeSeries.elementAt(i);
				
				avgTimeSeries.setElementAt(value / counter, i);
			}
			
			return avgTimeSeries;
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			
			return null;
		}
		finally
		{
			try
			{
				if (br != null)
				{ 
					br.close(); 
				}
			}
			catch(Exception e){}
		}
	}
	
}
